Webpack4 和 Babel 7 全新配置

前阵子自己开坑写了个 npm 开源的交互组件,按照 webpack 的配置流程走了一遍,打包遇到了各种坑。根据命令行的报错逐个排查,发现 babel 升级到 7.x 之后有坑。为了更好的记录遇到的问题以及解决方案,这里以 webpack4.x 的全套配置流程为方式重新梳理一遍

webpack 和 babel 浅析

在梳理配置流程之前我们先来大致过一下 webpack 和 babel 的基础理论,如果想更详细的了解,可以去查阅一下 webpack 官方文档 Babel 官方文档

webpack

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

image

也就是说,一切静态资源,皆可打包。webpack 可以说是现代前端发展的基石

那为什么需要 webpack 来打包呢? webpack 到底发挥的是什么作用?

  • webpack 可以通过分析项目结构,找到 JavaScript 模块以及其它一些浏览器不能直接运行的扩展语言( Scss , TS 等),并将其打包为合适的格式以供旧浏览器使用

  • 以往的前端技术,是用jQuery、html、css等来开发静态页面;而现在的,我们讲的都是 MVVM 框架,数据驱动界面,webpack 可以将现在 js 开发中的各种新型有用的技术集合打包

在如今功能丰富的页面下,都有着复杂的 JavaScript 代码和一大堆依赖包,为了简化开发的复杂度,前端社区出现了很多有益于提高开发效率的方法

比如我们熟悉的模块化开发,把复杂的程序细化为小的文件;scss / less 等 css 预处理器

这些开发方式都需要额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为 webpack 这类工具的出现提供了需求

babel

Babel 是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码

Babel 可以转换 JSX 语法,可以将 ES6 的代码转换为 ES5

babel 的目的就是为了解决浏览器的自身对于 es 语言的差异性而带来的一款工具,有了 babel 就首先不用担心旧浏览器不支持 es 语言这件事,其实最重要的不是支持,而是解决差异性,这种差异性不仅介于浏览器之间,对于 node 这样的环境也会存在这样的问题,各个 node 版本对于 es 的支持,或者对于 es 的一些尚未提交的草案的支持都是不同的,所以不论是浏览器下还是 node 下都需要到使用 babel 的场景

尤其是现在 Javascript 主要是用 ES6 编写的,为了更好适应各种旧浏览器,我们需要进行转换处理,这个转换的步骤也就是 transpiling(转译)

webpack 不知道如何进行转换但是有 loader (加载器),也就是转译器

babel-loader 是一个 webpack 的 loader (加载器),用于将 ES6 及以上版本转译至 ES5

使用 loader 之前,我们需要安装一堆依赖项,尤其是:

  • babel-core
  • babel-loader
  • babel-preset-env 用于将 Javascript ES6 代码编译为 ES5

webpack 4 变化

这里主要看看我们用的比较多的两则变化:

webpack 4 既不必须定义 entry point(入口点) ,也不必须定义 output file(输出文件)

entry point (入口点):webpack 寻找开始构建 Javascript 包的文件
output file (输出文件):webpack 构建完成输出 Javascript 包的文件

但是从 webpack 4 开始,这两个属性不是必须定义的,它有一个默认值,会将 ./src/index.js 作为默认入口点, ./dist/main.js 作为默认输出模块包

当然,我们也可以覆盖默认 entry point(入口点) 和 默认 output(输出) ,只需要在 package.json 中配置它们

module.exports = {
  entry: './src/index.js', 
  output: { 
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

或者

"scripts": {
  "dev": "webpack --mode development ./src/js/index.js --output ./dist/main.js",
  "build": "webpack --mode production ./src/js/index.js --output ./dist/main.js"
}

webpack 4 引入了 production(生产) 和 development(开发) 模式

拥有两个配置文件在 webpack 中是常见模式,但在 webpack 4 中,我们可以在没有一行配置的情况下完成,仅仅只需要在 package.json 中填充 script 部分

"scripts": {
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
}

然后运行

npm run dev

打开 ./dist/main.js,会发现有一个没有压缩的 bundle(包)文件

接着运行

npm run build

再次打开 ./dist/main.js,没错,bundle(包)压缩了!

production mode (生产模式) 可以开箱即用地进行各种优化。 包括压缩,作用域提升,tree-shaking 等
development mode (开发模式) 针对速度进行了优化,仅仅提供了一种不压缩的 bundle

环境搭建

这里使用 npm 来安装 webpack

npm i webpack webpack-cli -g

在 webpack3 中, webpack 和 cli 是在同一个包中,但在第4版中,为了更好的管理,已经将两者分开

初始化项目和配置

npm init -y
npm i webpack webpack-cli - D

部署配置

修改我们项目的 package.json ,使用 npm run build 启动 webpack

"scripts": {
  "build": "webpack --mode production"
},
"devDependencies": {
  "webpack": "^4.28.3",
  "webpack-cli": "^3.2.1"
}

我们在项目实战中应该很熟悉,类似于 vue-cli 或者是 umijs 这样的脚手架在打包的时候都会通过配置自动生成项目,通常都是在 src 文件夹下进行项目的开发,然后运行 npm run build 打包并生成我们的 dist 文件

比如我们在 src 目录下新建一个 index.js 文件,写上

console.log('我是测试案例')

然后运行 npm run build ,会发现新增了一个 dist 目录,里面存放着 webpack 打包好后的 main.js 文件

配置流程

回想我们之前做过的项目,一般会打包 src 下的什么文件呢?

  • 发布时需要的 html, css, js
  • css 预编译器 stylus / less / sass
  • es6 的高级语法
  • react 的 jsx 语法
  • 图片资源 .png, .gif, .ico, .jpg
  • 文件间的 require
  • 别名 @ 等修饰符
  • webpack dev server

下面就跟着上述几点来依次在 webpack 的 webpack.config.js 中进行配置,以 commonJS 模块化机制向外输出

module.exports = {}

html 配置

这里就不使用默认配置了,自行定义好入口 entry 和出口 output 。通俗的理解就是,webpack 相当于一个工厂,进入相当于把各种各样的原料放入我们的工厂,然后工厂进行一系列的打包操作,将打包好的东西向外输出,就可以出售了(上线)

module.exports = {
  entry: './src/index.js', //入口文件
  output: {  // 出口定义
    path: path.resolve(__dirname, 'dist'), // 输出文件的目标路径
    filename: '[name].js' // 文件名[name].js默认
  }
}

HTML 打包需要安装插件 html-webpack-plugin,它将会创建一个 index.html 文件,有了这个插件,我们就可以不用手动创建一个 index.html 文件了

npm i html-webpack-plugin -D

然后在 webpack.config.js 中引入

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ...
  plugins: [  // 插进的引用, 压缩,分离美化
    new HtmlWebpackPlugin({  // 将模板html的头部和尾部添加css和js模板
      file: 'index.html' // 输出文件的文件名称,默认为index.html
    }),
  ],
}

配置好后,在终端输入 npm run build 后 webpack 将 html 打包好并且自动将 js 引进来了

<body>
  <script type="text/javascript" src="main.js"></script>
</body>

在 dist 目录下启动本地服务器测试一下,运行 http-server,然后在浏览器中打开一个 ip 地址的 8080 端口,就可以在浏览器中看到我们的 Hello World 了,也就是我们上线的页面

css 配置

在我们的项目应用中,往往为了提高编程效率,使用一些预编译器,比如 stylus 、less 、sass 等,这里主要以 less 和 css 为例进行说明

在 src 下新建 index.html 、main.css 、style.less 文件

<body>
  <p class="title">Hello Webpack-4-cli !/p>
</body>
body {
  background: gray;
}
.title {
  color: red;
}

在 index.js 中引入

import style from "./main.css"
import style2 from "./style.less"

然后安装文件 css-loader 、 sass-loader 和 sass

npm i css-loader less less-loader style-loader -D

因为想让 css 在 dist 目录下和 HTML 分离,所以这是还需引入 extract-text-webpack-plugin

npm i extract-text-webpack-plugin -D

需要注意的是,这里有一个坑,因为 extract-text-webpack-plugin 最新版本为 3.0.2 ,这个版本还没有适应 webpack 4 的版本

解决办法:使用 4.0 beta 版

npm i extract-text-webpack-plugin@next -D

会下载到 extract-text-webpack-plugin@4.0.0-beta,安装好后,我们开始配置webpack.config.js文件

但是即便是使用 extract-text-webpack-plugin@4.0.0-beta 代替,还是会有一些问题,所以可以用另一个插件来代替,webpack4 可以使用 mini-css-extract-plugin 这个插件来单独打包 css

npm i mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 打包的css拆分,将一部分抽离出来  
module.exports = {
  // ...
  module: { // 模块的相关配置
    rules: [ // 根据文件的后缀提供一个loader,解析规则
      {
        test: /\.(css|less)$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", 'less-loader']
      },
  ]},
  plugins: [
    new HtmlWebpackPlugin({
      file: 'index.html',
      template: 'src/index.html' // 本地模板文件的位置,支持加载器(如handlebars、ejs、undersore、html等)
    }),
    new MiniCssExtractPlugin({ // [name] 默认  也可以自定义name  声明使用
      filename: "[name].css",
      chunkFilename: "[id].css"
    }),
  ],
}

打包之后 http-server , 发现我们的样式已经生效了,.less 的文件也被编译到 .css 文件中了,并且 css 部分已经从 html 中分离出来

js 配置

为了让更多的旧浏览器适应 es6 的开发语法,需要引入 babel 来把 es6 的代码编译为 es5 。在根目录下新建 .babelrc

{"presets": ["env"]}

在 index.js 中添加代码

const arr = [1, 2, 3]
const iAmJavascriptES6 = () => console.log(...arr)
window.iAmJavascriptES6 = iAmJavascriptES6

然后安装文件

npm i babel-loader babel-core  abel-preset-env -D

接下来在 webpack.config.js 中配置

module.exports = {
  // ...
  module: { // 模块的相关配置
    rules: [ // 根据文件的后缀提供一个loader,解析规则
      {
        test: /\.js$/,
        exclude: /node_modules/, // 不匹配选项(优先级高于test和include)
        use: 'babel-loader'
      },
  ]}
  // ...
}

这里会出现一个大坑,因为我们的 babel 已经升级到了一个大版本 - 7.x

Error: Cannot find module '@babel/core'
babel-loader@8 requires Babel 7.x (the package '@babel/core').
If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.

没找到 @babel/core ,这里把 babel-core 卸载,并安装 @babel/core

npm un babel-core
npm i -D @babel/core

同理,也需要将 babel-preset-env 卸载后重新安装,最终安装好的文件是

"devDependencies": {
  "@babel/core": "^7.2.2",
  "@babel/plugin-transform-runtime": "^7.2.0",
  "@babel/preset-env": "^7.2.3",
  "@babel/runtime": "^7.2.0",
  "babel-loader": "^8.0.5",
}

babel 舍弃了以前的 babel-- 的命名方式,改成了 @babel/-

修改完依赖和 .babelrc 文件后就能正常启动项目了

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]
}

为 React 项目配置 webpack 4

安装 React :

npm i react react-dom --D

因为前面已经安装了 babel-loader@babel/core@babel/preset-env , 所以只需安装babel-preset-react , 同上面一样的情况,在 babel 7 中,我们需要安装 @babel/preset-react

npm i @babel/preset-react --D
  • babel-loader : 使用 Babel 转换 JavaScript依赖关系的 Webpack 加载器
  • @babel/core : 即 babel-core,将 ES6 代码转换为 ES5
  • @babel/preset-env : 即 babel-preset-env,根据您要支持的浏览器,决定使用哪些 transformations / plugins 和 polyfills,例如为旧浏览器提供现代浏览器的新特性
  • @babel/preset-react : 即 babel-preset-react,针对所有 React 插件的 Babel 预设,例如将 JSX 转换为函数

再次强调:babel 7 使用了 @babel 命名空间来区分官方包,因此以前的官方包 babel-xxx 改成了 @babel/xxx

修改 webpack.config.js 和 .babelrc 文件

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/, // 不匹配选项(优先级高于test和include)
        use: 'babel-loader'
      },
  ]}
  // ...
}
{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]
}

然后在 src 目录下新建一个 App.js 的 react 组件

import React from "react"
import ReactDOM from "react-dom"
const App = () => {
  return (
    <div>
      <p>Hello React!</p>
    </div>
  )
}
export default App
ReactDOM.render(<App />, document.getElementById("app"))

接下来在 index.js 文件中 import(导入) 组件

import App from "./App"

在 index.html 中添加代码

<body>
  <p class="title">Hello Webpack-4-cli !</p>
  <div id="app"></div>
</body>

再次构建 npm run build 即可

如果想详细了解可以查阅这篇文章 React 教程:如何使用 webpack 4 和 Babel 构建 React 应用(2018)

图片资源的配置

在src目录下新建一个assets文件,里面放置图片

安装依赖文件

npm i file-loader -D

然后在 webpack.config.js 中添加配置

module.exports = {
  // ...
  module: {
    rules: [
      { // 图片loader
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader' // 根据文件地址加载文件
          }
        ]
      }
  ]}
  // ...
}

然后在 index.js 文件中引入

import img from './assets/star.png'

var oImg = new Image()
// oImg.src = require('./assets/star.png') // 当成模块引入图片
oImg.src = img
document.getElementById('image').appendChild(oImg)

然后在 index.html 中添加代码

<body>
  <p class="title">Hello Webpack-4-cli !</p>
  <div id="app"></div>
  <div id="image"></div>
</body>

别名(@)的配置

在 src 目录下新建一个文件夹 utils , 然后在 utils 中新建文件 format.js

module.exports = function format(chars) {
  return chars.toUpperCase()
}

如果我们要在 src/index.js 中引入这个 format.js 文件呢,理所当然是不是想这么写

import * as format from './utils/format'

这样写确实没错,但如果想使用别名代替呢?比如在 vue-cli 项目中常用的 @ 一个文件路径,其意思就是在 src 目录下,同样的方法,我们可以在 webpack.config.js 中添加配置

module.exports = {
  // ...
  resolve: { // 解析模块的可选项
    // modules: [ ]//模块的查找目录 配置其他的css等文件
    extensions: [".js", ".json", ".jsx", ".less", ".css"], // 用到文件的扩展名
    alias: { // 模快别名列表
      utils: path.resolve(__dirname,'src/utils')
    }
  },
  // ...
}

就可以在 index.js 中修改引入方式了

const format = require('utils/format')

CopyWebpackPlugin 插件和 webpack.ProvidePlugin 插件

  • 使用 CopyWebpackPlugin 可以将 src 下其他的不需要打包的文件直接复制到 dist 目录下
  • 使用 webpack.ProvidePlugin 可以在全局引用 lodash 这类的工具库,省去了import

在 webpack.config.js 中添加配置

module.exports = {
  // ...
  plugins: [
    new CopyWebpackPlugin([
      { from:'src/assets/redStar.png',to: 'redStar.png' }
    ]),
    new webpack.ProvidePlugin({
        '_': 'lodash'  // 引用 webpack
    })
  ],
  // ...
}

webpack dev server

想象一下,如果我们对代码进行更改,都需要 npm run dev,会不会远远降低我们的开发效率

如果使用 webpack dev server 配置,浏览器会自动启动你的应用程序

每次更改文件时,它都会自动刷新浏览器的窗口,比如 vue-cli 中,我们启动监听 npm run dev 时,可以监控我们 src 下文件的改动,那是怎么做到的呢?

在 webpack 里其实创建了一个 node 进程,通过 webpack-dev-server 其内部封装了一个 node 的 express 模块

首先安装包

npm i webpack-dev-server --D

接下来打开 package.json 并调整 scripts

"scripts": {
  "dev": "webpack-dev-server --mode development --open",
  "build": "webpack --mode production"
}

然后在 webpack.config.js 中添加配置

module.exports = {
  // ...
  devServer: { // 服务于webpack-dev-server  内部封装了一个express 
    port: '8888',
    before(app) {
      app.get('/api/test.json', (req, res) => {
        res.json({
          code: 200,
          message: 'Hello World'
        })
      })
    }
  }
  // ...
}

启动监听 npm run dev ,就可以看到 webpack dev server 在浏览器中启动了应用项目

完整代码链接

webpack-4-cli

学习资料

手写一个webpack4.0配置
从零配置到生产发布(2018)
使用 Webpack 4 和 Babel 7 从头开始创建 React 应用程序
babel 7.x 和 webpack 4.x 配置 vue 项目
阮一峰 Webpack 教程